Skip to content

Add more pallet-psm tests#11805

Open
rockbmb wants to merge 10 commits intomasterfrom
psm-try-state-checks
Open

Add more pallet-psm tests#11805
rockbmb wants to merge 10 commits intomasterfrom
psm-try-state-checks

Conversation

@rockbmb
Copy link
Copy Markdown
Contributor

@rockbmb rockbmb commented Apr 16, 2026

Description

This PR is a follow-up to #11068. It contains suggestions - for try_state checks and pallet unit tests - that were not made before it was merged.
It is also a companion to open-web3-stack/polkadot-ecosystem-tests#592 .

Any comments or suggestions appreciated (pinging @lrazovic since you authored #11068).

Integration

N/A

Review Notes

  • add more try_state checks to pallet-psm
  • add a test that verifies expected behavior of fn do_try_state on valid state
  • add unit tests that verify do_try_state fails appropriately on several different kinds of invalid pallet-psm states
  • add cross-pallet issuance check (Check 4: total pUSD issuance >= total PSM debt)
  • cover some additional edge cases regarding multi-asset ceiling boundaries
  • add fuzzer infrastructure for pallet-psm
    • coverage-guided libFuzzer target (fuzz_targets/psm.rs): multi-block sequences, state-aware amount generation, weighted call selection, do_try_state validation after each block
    • stateful property-based tester (fuzz_targets/psm_stateful.rs): deterministic seed, full state snapshots, dynamic candidate generation based on preconditions, do_try_state validation after every command
    • both validate all do_try_state invariants including the cross-pallet issuance check
    • mock.rs extended with fuzzing feature gate, additional assets (DAI, USDP, FRAX), and fuzz_helpers module
    • fuzz/README.md documenting both fuzzers, when to use which, and build instructions

rockbmb added 3 commits April 16, 2026 18:00
Fix a check from `log::warn! -> ensure!`, add more comments to each,
and order them for legibility.
@rockbmb rockbmb self-assigned this Apr 16, 2026
@rockbmb rockbmb requested a review from a team as a code owner April 16, 2026 22:07
@rockbmb rockbmb added T2-pallets This PR/Issue is related to a particular pallet. T10-tests This PR/Issue is related to tests. labels Apr 16, 2026
@socket-security
Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedcargo/​libfuzzer-sys@​0.4.124710093100100
Addedcargo/​arbitrary@​1.4.210010093100100
Addedcargo/​rand@​0.8.610010093100100

View full report

@rockbmb
Copy link
Copy Markdown
Contributor Author

rockbmb commented Apr 20, 2026

Fuzzer Validation

The below text demonstrates the two fuzzers from pallet-psm functioning by locally introducing some errors in the pallet's source, and running them against the patched code.

Each log line shows dispatch=OK/ERR (did the extrinsic succeed) and invariant=OK/VIOLATED (did do_try_state pass after the command). PRE and POST show pallet state before and after execution.


Example 1: Redeem does not decrease PsmDebt

PsmDebt::<T>::mutate(...) in redeem() commented out. External tokens leave the PSM, pUSD is burned, but the debt ledger stays unchanged.

Caught by Check 2 (reserve >= debt).

Stateful tester — seed=4, caught at command 5
[   0] Mint(acct=8, USDC, 500100000000) dispatch=ERR invariant=OK
       PRE  debt=0/10.0M issuance=0/20.0M reserve=0
       POST debt=0/10.0M issuance=0/20.0M reserve=0
[   1] RemoveAsset(USDC) dispatch=OK invariant=OK
       PRE  debt=0/10.0M issuance=0/20.0M reserve=0
       POST debt=0/10.0M issuance=0/20.0M reserve=0
[   2] Mint(acct=3, USDT, 499900000000) dispatch=OK invariant=OK
       PRE  debt=0/10.0M issuance=0/20.0M reserve=0
       POST debt=499.9K/10.0M issuance=499.9K/20.0M reserve=499.9K
[   3] SetMintFee(USDT, 100.000%) dispatch=OK invariant=OK
       PRE  debt=499.9K/10.0M issuance=499.9K/20.0M reserve=499.9K
       POST debt=499.9K/10.0M issuance=499.9K/20.0M reserve=499.9K
[   4] Mint(acct=8, USDT, 250000000000) dispatch=OK invariant=OK
       PRE  debt=499.9K/10.0M issuance=499.9K/20.0M reserve=499.9K
       POST debt=749.9K/10.0M issuance=749.9K/20.0M reserve=749.9K
[   5] Redeem(acct=3, USDT, 494900999999) dispatch=OK invariant=VIOLATED: Other("PSM reserve is less than tracked debt for an asset")
       PRE  debt=749.9K/10.0M issuance=749.9K/20.0M reserve=749.9K
       POST debt=749.9K/10.0M issuance=259.9K/20.0M reserve=259.9K

Commands 2 and 4 build debt and reserves to 749.9K. Command 5 redeems: reserve drops to 259.9K but debt stays at 749.9K. 749.9K > 259.9K — violated.

libFuzzer — found independently via coverage-guided mutation

Found within ~512 iterations from the existing corpus:

#512    pulse  cov: 4701 ft: 16984 corp: 259/810b exec/s: 18 rss: 474Mb
thread '<unnamed>' panicked at fuzz_targets/psm.rs:527:42:
PSM invariant violated: Other("PSM reserve is less than tracked debt for an asset")
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
==308486== ERROR: libFuzzer: deadly signal
SUMMARY: libFuzzer: deadly signal
MS: 0 ; base unit: 0000000000000000000000000000000000000000
artifact_prefix='fuzz/artifacts/psm/'; Test unit written to fuzz/artifacts/psm/crash-...

Reproduce:

cd substrate/frame/psm
# Comment out PsmDebt::mutate in redeem() at line 620, then:
cargo +nightly fuzz run psm

Example 2: Redeem burns pusd_amount instead of external_to_user

burn_from(&who, external_to_user, ...) in redeem() changed to burn_from(&who, pusd_amount, ...). The fee-inclusive amount is burned from the caller instead of just the net amount.

Caught by Check 4 (total_issuance >= total_psm_debt).

Stateful tester — seed=42, caught at command 3
[   0] Mint(acct=6, USDT, 500000000001) dispatch=ERR invariant=OK
       PRE  debt=0/10.0M issuance=0/20.0M reserve=0
       POST debt=0/10.0M issuance=0/20.0M reserve=0
[   1] SetRedeemFee(USDT, 100.000%) dispatch=OK invariant=OK
       PRE  debt=0/10.0M issuance=0/20.0M reserve=0
       POST debt=0/10.0M issuance=0/20.0M reserve=0
[   2] Mint(acct=8, USDC, 499900000000) dispatch=OK invariant=OK
       PRE  debt=0/10.0M issuance=0/20.0M reserve=0
       POST debt=499.9K/10.0M issuance=499.9K/20.0M reserve=499.9K
[   3] Redeem(acct=8, USDC, 321172187687) dispatch=OK invariant=VIOLATED: Other("Total pUSD issuance is less than total PSM debt — balance sheet does not close")
       PRE  debt=499.9K/10.0M issuance=499.9K/20.0M reserve=499.9K
       POST debt=181.9K/10.0M issuance=178.7K/20.0M reserve=181.9K

Command 2 mints 499.9K. Command 3 redeems: the bug burns more pUSD than the debt ledger accounts for. POST shows debt=181.9K but issuance=178.7K. 178.7K < 181.9K — violated.


Reproduce

cd substrate/frame/psm

# Bug 1 — comment out PsmDebt::mutate in redeem() at line 620, then:
cargo run --bin psm_stateful --manifest-path fuzz/Cargo.toml -- 4 20
# or via libFuzzer:
cargo +nightly fuzz run psm

# Bug 2 — change external_to_user to pusd_amount in burn_from at line 597, then:
cargo run --bin psm_stateful --manifest-path fuzz/Cargo.toml -- 42 20

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T2-pallets This PR/Issue is related to a particular pallet. T10-tests This PR/Issue is related to tests.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant